2023 熵密杯
今天和同事组队“金盾检测”一起参加了在河南郑州举办的首届熵密杯线下赛,是之前没有接触过的闯关赛制,有点类似渗透,关卡之间环环相扣,也都和密码学应用紧密相关。不过很遗憾在之前的比赛中对国密的接触并不多,因此有很多知识盲区,导致此次比赛也未能解出全部赛题,所以玩的也不是很尽兴,希望下次还能再来!
初始谜题
话不多说,我们先来看看赛题。首先是初始谜题,我们需要在靶场开启一个场景,会给到一个 ip 和 port;然后我们还可以下载到一个附件,里面包含一个客户端,我们在客户端里输入场景的 ip 和 port 就可以获得题目,题目使用 SM4 CBC-MAC 体制,计算了两个 32 字节消息的 MAC值,要求我们给出一个 64 字节的消息和对应的 MAC 值。具体内容如下
1 | 请输入谜题服务器IP地址(Please input Puzzle Server IP Address) |
由于 SM4 CBC-MAC 的计算是需要密钥的,而我们没有密钥也就无法计算任意消息的 MAC 值,这里肯定是需要去特殊构造的。然而一开始并没有什么头绪,直到比我熟悉国密的队友提示说 SM4 CBC-MAC 的初始 iv 是全零。嗯?全零,!!!!思路来了。
我们知道 CBC 的模式大概是这样的
而 SM4 CBC-MAC 是以最后一组的密文作为 MAC 值。于是对于这题而言,我们已知的信息是这样的
既然没有密钥,那么这题肯定是要用现有的信息了。另外注意到,两个消息都是32字节,却让我们给一个64字节的消息,题目已经推着我们把两个消息合并到一起了。
两个消息合并,那么最后的 MAC 值就用 MAC2好了,于是我们就需要处理一下拼接处的问题。原本 MSG2 的第一分组的异或向量是全零,即有 $Enc(0 \oplus p_2) = c_1 $ (以上图为例)
如果直接将两个消息拼接计算 MAC 值,那么根据 CBC 的模式, MSG2 的第一分组的异或向量就是 MAC1,即有 $Enc(MAC_1\oplus p_2)$,那么这显然会影响到 ciphertext1,继而改变了最后的 MAC 值。所以我们要”消去“这个影响。改变的方法也很简单,我们只需要改变对应的 $p_2$ 为 $new \ p_2 = p_2 \oplus MAC_1$,即我们的明文消息为 $MSG_1||MAC_1\oplus \ p_2||p_3$,这样在两个消息拼接处,我们就有 $Enc(MAC_1 \oplus new \ p_2) = Enc(MAC_1 \oplus MAC_1 \oplus p_2) = Enc(0 \oplus p_2) = c_1$,于是最后消息的 MAC 值就是原来 MSG2 的 MAC 值: $MAC_2$
我们构造的消息:原始 MSG1:e55e3e24a3ae7797808fdca05a16ac15eb5fa2e6185c23a814a35ba32b4637c2
计算 $MAC_1\oplus \ p_2$:
1 | hex(0x0712c867aa6ec7c1bb2b66312367b2c8^0xd8d94f33797e1f41cab9217793b2d0f0) |
在拼上 $p_3$:2b93d46c2ead104dce4bfec453767719
得到最终的消息:e55e3e24a3ae7797808fdca05a16ac15eb5fa2e6185c23a814a35ba32b4637c2dfcb8754d310d88071924746b0d562382b93d46c2ead104dce4bfec453767719
然后输出 $MAC_2$ 就能通过验证,如下所示
1 | 请输入您的MSG3(64字节,128个Hex,不要添加空格!)(Please input your 64bytes MSG3(64 bytes,128 hexs,don't using space)): |
于是我们拿到了第一个 flag,以及一个 Gitea 的账号密码,我们根据网络拓扑,来到一个 Gitea 的登录页面,输入给到的账号密码可以进入一个仓库。其中有两个文件,一个是题目附件,一个是一份openssl 的源码。
第一关
题目附件包含一个加密的压缩包,里面含有一些数据包和 flag1。额外还给出了一个密文
1 | <!!!!!!!!!!!!解密!解开我,你将获得全部信息!!!!!!!!!!!!!!!!!!> |
和加密的源代码
1 |
|
一共十六轮,每一轮包含比特反转,位置置换,循环位移,密钥异或,计算单位是一个字节。可以看到都是分组密码的一些组件,我们对应的逆回去就可以了。
先逆密钥异或:加密是明文的每个字节异或 0x78 * round & 0xFF
1 | void xorWithKeys(unsigned char* password, unsigned int round) { |
那么解密就是把轮数 round
的值反着用一遍就好
1 | newa= "" |
接着循环移位,加密是每个字节向左循环移位 3 位
1 | void leftShiftBytes(unsigned char* password) { |
那我们右移回去
1 | newa = "" |
然后位置置换
1 | void swapPositions(unsigned char* password) { |
根据代码,举例当 i = 0,positions[i] 为 13,于是 temp[13] = password[0]。因此在解密的时候,明文的第 0 位,就是密文的第 positions[0],也就是第 13 位。于是
1 | table = [13, 4, 0, 5,2, 12, 11, 8,10, 6, 1, 9,3, 15, 7, 14] |
最后是比特反转,这个我们反逆回来就好了。整合一下
1 | from Crypto.Util.number import * |
运行后得到压缩包密码 pdksidicndjh%^&6
,解密压缩包获得 flag:flag1{52e0acce-1e87-c966-43a4-59995df10b10},和两个流量包 数字签名系统调试数据包.pcapng、数字签名前置系统调试数据包.pcapng,和两份源代码 login.go、download.go。
第二关
根据靶场上的网络拓扑,我们能进入数字签名前置系统
可以看到需要输入用户名,证书,以及对应的私钥值。
根据上一关,我们已经拥有了这里的部分源码
login.go
1 | func CertLogin(c *gin.Context, conf config.Config) { |
download.go
1 |
|
根据 download.go 应该是登录成功后的一个下载文件页面。所以目前我们需要将注意力放在login.go上。
具体流程为:
- 检查是否 post 了随机数 randNumStr,如果有则跳到第 16 步,否则继续下面的判断。
- 检查是否输入了用户名
- 检查是否上传了文件
- 检查上传的文件是否为证书类型
- 检查该证书文件能否打开
- 检查该证书文件能否读取
- 检查该证书文件能否以pem格式解析
- 检查该证书是否为 gmx509 格式(应该是,对go语言不是特别熟)
- 检查该证书是否在有效期内
- 检查 username 是否等于证书里的 CommonName
- 检查该证书的 serialNumber 是否为 0
- 检查该证书的 CommonName 是否为服务端配置文件中设定的 IssuerName
- 检查该证书是否有 “1.2.3.4” 类型的拓展,以及值是否和服务端配置文件中设置的相等。
- 检查该 username 是否为系统的注册用户
- 如果上面的检查都通过了则随机生成一个随机数返回给用户,并以该随机数和证书作为键值对存入字典 Cache 中。
- 读取用户传入的签名 signature
- 以随机数 randNumStr 作为键在 Cache 中获取对应证书
- 再次检查该证书文件能否以pem格式解析
- 再次检查该证书是否为 gmx509 格式
- 从证书中获取对应的 sm2 参数
- 十六进制解码用户的 signature,并获取对应的 r,s 值
- 使用公钥验证用户的签名
在上面任何一项检查不通过都会报错返回,并给出相应的错误代码(这实际上是不安全的,理论上应该只单纯的给出错误的返回,让用户无法判断错误点在哪,不过在这个场景中并不太能利用),全部通过则能登录成功。
然后在 数字签名前置系统调试数据包.pcapng 中我们看见了 admin1 的一个登录过程
1 | POST /api/certLogin HTTP/1.1 |
我们可以看到 admin1 的证书,随机数 415979 以及对应的签名 c4f6d124ebcf0969ae0d86f234680ef7730f62f83d5fa257f6734d80537d63eff7004f1339d2d13368f61ff8327c9e77d2c6a48e85c73a9d739811aeda5341ac
在这里我们卡了很久,
一开始的想法,首先 SM2 是安全的,解 ECDLP 是不可能的了,不行;
这里只有一组签名数据,临时密钥重用攻击,不行;
用户的签名在网页前端进行,审计了一下签名的js代码,临时密钥的生成没有问题,不行;
最后我们注意到随机数的生成和 Cache 机制
1 | if sysUserName == subjectName { |
由于随机数只有一百万个可能,如果说我们传入 admin1 的证书,并且当返回值正好是数据包中的 415979,那么由于我们也拥有对应的签名,我们就可以实现一次重入攻击,理论可行!
1 | import requests |
可惜,在赛场上运气不好,一直没有成功。而且成功后也面临一个问题,由于直接网页端访问需要我们输入私钥文件,因此我们后面也只能一直用脚本交互,会十分的不方便。于是我们再一次卡住了。
直到两点半放出提示:直接替换证书的公钥。
我们看到前面的检查,确实没有检查包括指纹、使用者密钥标识符、授权密钥标识符等上游信任链的问题。但是又一个问题出现了,我不熟悉 pem 文件的格式,所以并不知道怎么替换公钥。
于是使用笨方法,首先保存证书的 base64 编码,改后缀为 .cer,我们可以用 windows 直接打开看到
现在回想起来,这就是一个很明显的提示了,明明不受信任的证书,在该前置系统上却能上传成功,说明检查的就不是很完整。随后看到详细信息,找到公钥
是 04 75 … b7 54
我们再将上述 base64 编码解码再转为十六进制编码,得到
于是我们本地生成一个私钥,然后将对应公钥替换进去就可以了。(后面再十六进制解码,换行换一下)
这里我生成的私钥为 $2^{256}-1$ ,对应的证书文件为
1 | -----BEGIN CERTIFICATE----- |
随后上传证书,私钥为
b64encode(long_to_bytes(2**256-1)) -> //////////////////////////////////////////8=
就可以登录成功,然后再次下载到一个加密的压缩包,和一个密文文件
1 | <!!!!!!!!!!!!解密!解开我,你将获得全部信息!!!!!!!!!!!!!!!!!!> |
这里没给额外的加密代码,猜想应该还是用的前面的加密方式,所以我们用前面的解密脚本再次解密,得到压缩包 eufi*@(%$DKK884+
,打开压缩包,获得 flag,flag2{7b84588a-0a54-5639-a828-9062e8a7f6c2}
做到这里就止步了,全场除了 0ops 和 AAA,其他队伍最多也就是到了这一步。遗憾的是前面两道题都是第四个解出来的没有拿到三血加分,第三道题刚开始思路歪了,提示放出来后众生平等,不过由于对 pem 文件不熟悉,openssl玩的也不是很溜,用笨方法慢了些,所以最后只拿了个第14。遗憾遗憾。
后续关卡的猜测
虽然没解出来后面的题目,但对后面的关卡也做了一定的探索。(不过由于是赛后总结的,没有环境,只好用口头叙述一下。整个比赛最有意思的应该也是在这里,可惜太菜了,体验不到)在通过第二关后,得到了一份火狐浏览器的代理工具和对应的操作指南,另外还有数字签名系统的签名和验签的 C 源码。然后登录数字签名前置系统后会有一个进入数字签名系统的链接,点进去是跨 B 段的,所以无法访问,要用代理。不过代理没有给用户名和密码,我们需要根据前面还没有用到的 数字签名系统调试数据包.pcapng 来进行分析。根据题目的提示,调试包中有两次握手,均采用 ECDH 来协商密钥,其中服务端的公钥不变,客户端的公钥变了。另外公告提示说服务端使用的私钥可以在前面 Gitea 中的 openssl 源码中获取(0ops在得知该消息后立刻拿到了一血,估计前面是卡在这了)。然后我们需要根据这个私钥计算会话的预主密钥,构造好文件导入 wireshark 配置,随后解密会话,就能拿到用户名和密码,进入数字签名系统,猜测里面应该会有一个 flag3。【比赛时由于不会解析椭圆曲线的私钥文件,另外也没有找到对应的私钥文件,遂放弃】至于大赛的最后目标:伪造一个能通过数字签名系统的签名。查看了一下得到的签名和验签的 C 代码,里面给出了msg1和msg2,以及msg1的签名,猜测我们要计算处 msg2 的签名。然后我看到了 Gitea 中的改动,出题人对 openssl 项目中的 crypto/rand/drbg_lib.c 文件中一个生成随机数的函数进行了修改,将原本生成32字节随机数写死了,
1 | int RAND_DRBG_bytes(RAND_DRBG *drbg, unsigned char *out, size_t outlen) |
猜测应该是在数字签名系统计算 msg1 签名,生成临时密钥的时候调用了这个函数。于是我们可以在已知 msg1 的临时密钥和签名的情况下恢复私钥,然后计算 msg2 的签名,通过系统的验证。
【老规矩,题目相关的附件,公众号后台回复关键字:2023熵密杯】
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可联系QQ 643713081,也可以邮件至 643713081@qq.com
文章标题:2023 熵密杯
文章字数:5.8k
本文作者:Van1sh
发布时间:2023-08-11, 12:00:00
最后更新:2023-09-10, 22:39:16
原始链接:http://jayxv.github.io/2023/08/11/2023 熵密杯/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。